/*
 * File     : DniInternal.java
 * Created  : 23 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal;

import static org.objectweb.asm.Opcodes.BIPUSH;
import static org.objectweb.asm.Opcodes.ICONST_0;
import static org.objectweb.asm.Opcodes.SIPUSH;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.objectweb.asm.MethodVisitor;

import com.googlecode.dni.Allocator;
import com.googlecode.dni.DirectNativeInterface;
import com.googlecode.dni.InitialisationOption;
import com.googlecode.dni.callback.FunctionPointerFactory;
import com.googlecode.dni.callback.IllegalCallbackException;
import com.googlecode.dni.internal.MemoryAccess.MemoryAccessor;
import com.googlecode.dni.internal.call.LastError;
import com.googlecode.dni.internal.library.LibraryBuilder;
import com.googlecode.dni.internal.library.LibraryRecord;
import com.googlecode.dni.internal.platform.Platform;
import com.googlecode.dni.internal.platform.PlatformUtilities;
import com.googlecode.dni.internal.string.ByteNativeStringBuffer;
import com.googlecode.dni.internal.string.FastCharset;
import com.googlecode.dni.internal.string.IntNativeStringBuffer;
import com.googlecode.dni.internal.string.NativeStringImpl;
import com.googlecode.dni.internal.string.ShortNativeStringBuffer;
import com.googlecode.dni.internal.structure.RecursiveStructureException;
import com.googlecode.dni.internal.structure.StructureManager;
import com.googlecode.dni.internal.structure.StructureRecord;
import com.googlecode.dni.library.Function;
import com.googlecode.dni.library.IllegalLibraryException;
import com.googlecode.dni.library.Library;
import com.googlecode.dni.type.NativeObjectList;
import com.googlecode.dni.type.NativeString;
import com.googlecode.dni.type.NativeStringBuffer;
import com.googlecode.dni.type.Pointer;
import com.googlecode.dni.type.cenum.EnumBitFieldHelper;
import com.googlecode.dni.type.cenum.MaskBitFieldHelper;
import com.googlecode.dni.type.structure.Padding;
import com.googlecode.dni.type.structure.Structure;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;


/**
 * <p>
 *  Provides the central point of access for all the internal services.
 * </p>
 * <p>
 *  The intention is that all of the classes in the API section come through
 *  this gateway.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class DniInternal
{

    /** Whether debug mode is enabled. */
    public static final boolean DEBUG_MODE;

    /** Whether debug allocator mode is enabled. */
    public static final boolean DEBUG_ALLOCATOR_MODE;

    /** Testing object.  DO NOT USE IN PRODUCTION CODE. */
    public static final Testing TESTING;

    /** The platform. */
    public static final Platform PLATFORM;

    /** The allocator. */
    public static final Allocator ALLOCATOR;

    /** The accessor. */
    public static final MemoryAccessor MEMORY_ACCESSOR;

    /** Logger for DNI. */
    public static final Logger LOGGER = Logger.getLogger( "DNI" );

    /** Native byte order. */
    public static final ByteOrder NATIVE_ENDIAN = ByteOrder.nativeOrder();

    private static final Map< Class< ? >, LibraryRecord > LIBRARIES =
        new WeakHashMap< Class< ? >, LibraryRecord >();

    private static final Map< Class< ? >, FunctionPointerFactory< ? > > FUNCTION_POINTER_FACTORIES =
        new WeakHashMap< Class< ? >, FunctionPointerFactory< ? > >();

    private static final boolean DUMP_BYTE_CODE;

    private static final File DUMP_BYTE_CODE_DIR;

    /** Share a lock because libraries can depend on structures. */
    private static final Object LOCK = new Object();

    static
    {
        // Calling getOption() forces the main class to be initialised.  So it's
        // safe for a client of the library to make use of classes that call
        // directly into DniInternal.

        DEBUG_MODE = (Boolean) DirectNativeInterface.getOption( InitialisationOption.DEBUG_MODE );
        DEBUG_ALLOCATOR_MODE = (Boolean) DirectNativeInterface.getOption( InitialisationOption.ALLOCATOR_DEBUG_MODE );
        PLATFORM = PlatformUtilities.getPlatform();
        ALLOCATOR = MemoryAccess.ALLOCATOR;
        MEMORY_ACCESSOR = MemoryAccess.MEMORY_ACCESSOR;

        String dir = (String) DirectNativeInterface.getOption( InitialisationOption.DUMP_BYTE_CODE );
        if ( dir == null || dir.isEmpty() )
        {
            DUMP_BYTE_CODE = false;
            DUMP_BYTE_CODE_DIR = null;
        }
        else
        {
            DUMP_BYTE_CODE = true;
            DUMP_BYTE_CODE_DIR = new File( dir );
            boolean created = DUMP_BYTE_CODE_DIR.mkdirs();
            if ( !created && !DUMP_BYTE_CODE_DIR.isDirectory() )
            {
                LOGGER.warning( "Can't dump byte code to: " + dir );
            }
        }

        if ( !DniTypes.DNI_INTERNAL_TYPE.replace( '/', '.' ).equals( DniInternal.class.getName() ) )
        {
            throw new AssertionError( "Package has been renamed: " + DniTypes.DNI_INTERNAL_TYPE + " " + DniInternal.class.getName() );
        }

        if ( "true".equals( System.getProperty( "dni.internal.testing" ) ) )
        {
            TESTING = new Testing();
        }
        else
        {
            TESTING = null;
        }
    }

    /** Prevents instantiation. */
    private DniInternal()
    {
        throw new UnsupportedOperationException();
    }

    /** @return the default padding */
    public static Padding getDefaultPadding()
    {
        if ( TESTING != null )
        {
            return TESTING.padding;
        }

        return PLATFORM.getDefaultPadding();
    }

    /**
     * <p>
     *  Creates an instance of a library API, given its type.
     * </p>
     * <p>
     *  The native library specified by the {@link Library} annotation is first
     *  loaded, then an implementation of the given interface is created that
     *  will call the method's equivalent native function.
     * </p>
     * <p>
     *  See the class documentation for details of how interface methods require
     *  annotation to specify the native function's details.
     * </p>
     *
     * @param type
     *            the type of interface
     *
     * @return the record for the library
     *
     * @throws LinkageError
     *             if the library or libraries could not be loaded
     * @throws IllegalLibraryException
     *             if the given type is not an interface, or is not correctly
     *             annotated
     */
    @SuppressFBWarnings( value = "PMB_POSSIBLE_MEMORY_BLOAT",
                         justification = "v1.0 only supports loading, not unloading" )
    public static LibraryRecord getLibraryRecord( final Class< ? > type ) throws LinkageError
    {
        synchronized ( LOCK )
        {
            LibraryRecord record = LIBRARIES.get( type );
            if ( record == LibraryRecord.IN_PROGRESS )
            {
                throw new IllegalLibraryException( "Recursive call to create: " + type );
            }
            if ( record != null )
            {
                return record;
            }

            // don't permit recursive calls
            LIBRARIES.put( type, LibraryRecord.IN_PROGRESS );
            try
            {
                LibraryBuilder builder = LibraryBuilder.create( type, getLibraryImplName( type ) );
                record = builder.build();
            }
            finally
            {
                LIBRARIES.put( type, record );
            }

            return record;
        }
    }

    // TODO v1.0 Move these structure methods and hidden types into the 'structure' sub-package
    // That'll make them less exposed.  Some are dangerous in that they are
    // quite stateful.
    // There's also something else:  With recursive structures, the statefulness
    // is really tricky to get right.  There needs to be delayed initialisation
    // of impl types so that all in the cycle are loaded, then initialised.
    // The current impl does not work.

    /**
     * <p>
     *  Obtains the structure record for a structure type.
     * </p>
     *
     * @param type
     *            the type of structure
     *
     * @return a record for the structure
     *
     * @throws RecursiveStructureException
     *             if there is an attempt to make a recursive structure
     */
    public static StructureRecord getStructureRecord( final Class< ? extends Structure > type )
        throws RecursiveStructureException
    {
        synchronized ( LOCK )
        {
            return StructureManager.get( type );
        }
    }

    /**
     * Clears all structure records.  For use only in testing.
     */
    public static void clearStructureRecords()
    {
        synchronized ( LOCK )
        {
            StructureManager.clear();
        }
    }

    /**
     * <p>
     *  Obtains an abstract factory that will create thunks of the given type.
     * </p>
     *
     * @param <T>
     *            the type of call-back
     * @param type
     *            the type of call-back
     *
     * @return a factory for the type
     *
     * @throws IllegalCallbackException
     *             if the type is not an interface, does not have precisely one
     *             method, or is not correctly annotated
     */
    public static < T > FunctionPointerFactory< T > getFunctionPointerFactory( final Class< T > type )
    {
        synchronized ( LOCK )
        {
            FunctionPointerFactory< ? > thunkFactory = FUNCTION_POINTER_FACTORIES.get( type );
            if ( thunkFactory != null )
            {
                @SuppressWarnings( "unchecked" )
                FunctionPointerFactory< T > castkFactory = (FunctionPointerFactory< T >) thunkFactory;
                return castkFactory;
            }

            if ( true )
            {
                throw new UnsupportedOperationException( "FIXME" );
            }
            FunctionPointerFactory< T > castThunkFactory = null; //builder.createFactory();

            FUNCTION_POINTER_FACTORIES.put( type, castThunkFactory );

            return castThunkFactory;
        }
    }

    /**
     * Defines a class.
     *
     * @param name
     *            the name (byte-code format)
     * @param classLoader
     *            the class loader
     * @param classFile
     *            the class file
     *
     * @return the loaded class
     */
    public static Class< ? > defineClass( final String name, final ClassLoader classLoader, final byte[] classFile )
    {
        if ( DUMP_BYTE_CODE )
        {
            dumpByteCode( name, classFile );
        }

        return NativeUnsafeAccess.defineClass( name, classLoader, classFile, classFile.length );
    }

    /**
     * Creates a pointer list.
     *
     * @param pointer
     *            the base pointer
     * @param size
     *            the number of elements
     * @return a new instance
     */
    public static NativeObjectList< Pointer > createPointerList( final Pointer pointer,
                                                                 final int size )
    {
        return new PointerList( pointer.address(), size );
    }

    /**
     * Wraps a string.
     *
     * @param string
     *            the string
     * @param charset
     *            the charset
     *
     * @return a native string
     */
    public static NativeString wrapString( final String string, final String charset )
    {
        FastCharset fastCharset = FastCharset.getInstance( charset );
        int size = fastCharset.getByteCount( string );
        Pointer pointer = ALLOCATOR.allocate( size );
        fastCharset.encode( string, pointer.address() );

        return new NativeStringImpl( pointer, Integer.MAX_VALUE );
    }

    /**
     * Wraps a native string.
     *
     * @param pointer
     *            the pointer
     * @param maximumLength
     *            the maximum length
     *
     * @return a native string
     */
    public static NativeString wrapString( final Pointer pointer, final int maximumLength )
    {
        return new NativeStringImpl( pointer, maximumLength );
    }

    /**
     * <p>
     *  Creates a string buffer.
     * </p>
     *
     * @param capacity
     *            the capacity of the buffer, in bytes; must be at least 4
     * @param charset
     *            the charset to use; or {@link Library#DEFAULT_CHARSET}
     *            or {@link Library#WIDE_CHARSET}
     *
     * @return a native string
     */
    public static NativeStringBuffer createStringBuffer( final int capacity, final String charset )
    {
        if ( capacity < 4 )
        {
            throw new IllegalArgumentException( "capacity" );
        }

        FastCharset fastCharset = FastCharset.getInstance( charset );

        Pointer pointer = ALLOCATOR.allocate( capacity );
        NativeStringBuffer stringBuffer;
        boolean success = false;
        try
        {
            stringBuffer = createStringBufferImpl( capacity, fastCharset, pointer );
            success = true;
        }
        finally
        {
            if ( !success )
            {
                // failed to create buffer -> free memory now
                ALLOCATOR.free( pointer );
            }
        }

        stringBuffer.clear();
        return stringBuffer;
    }

    /**
     * <p>
     *  Creates a string buffer from existing memory.
     * </p>
     *
     * @param pointer
     *            the pointer to existing memory
     * @param capacity
     *            the capacity of the buffer, in bytes; must be at least 4
     * @param charset
     *            the charset to use; or {@link Library#DEFAULT_CHARSET}
     *            or {@link Library#WIDE_CHARSET}
     *
     * @return a native string
     */
    public static NativeStringBuffer createStringBuffer( final Pointer pointer, final int capacity, final String charset )
    {
        if ( pointer == null )
        {
            throw new NullPointerException();
        }
        if ( capacity < 4 )
        {
            throw new IllegalArgumentException( "capacity" );
        }

        return createStringBufferImpl( capacity, FastCharset.getInstance( charset ), pointer );
    }

    /**
     * <p>
     *  Creates a helper for dealing with a bit field segment containing an
     *  enum value.
     * </p>
     *
     * @param <E>
     *            the Java enum type
     * @param enumType
     *            the Java enum type
     * @param mask
     *            the bit mask to use
     * @param offset
     *            the bit offset (0 to 31 inclusive) to apply to the enum's
     *            values
     * @return a helper
     */
    public static < E extends Enum< E > >
    EnumBitFieldHelper< E > getEnumBitFieldHelper( final Class< E > enumType, final int mask, final int offset )
    {
        return new EnumBitFieldHelperImpl< E >( enumType, mask, offset );
    }

    /**
     * <p>
     *  Creates a helper for dealing with a bit field segment containing a mask.
     * </p>
     *
     * @param <E>
     *            the Java enum type
     * @param enumType
     *            the Java enum type
     * @param offset
     *            the bit offset (0 to 31 inclusive) to apply to the enum's
     *            values
     * @return a helper
     *
     * @see MaskBitFieldHelper
     */
    public static < E extends Enum< E > >
    MaskBitFieldHelper< E > getMaskBitFieldHelper( final Class< E > enumType, final int offset )
    {
        return new MaskBitFieldHelperImpl< E >( enumType, offset );
    }

    /**
     * <p>
     *  Reads the most-recently set 'last error' value on this thread.
     * </p>
     *
     * @return the last error value
     *
     * @see Function#lastError()
     */
    public static int getLastError()
    {
        return LastError.getLastError();
    }

    /**
     * <p>
     *  Resets the 'last error' value on this thread.
     * </p>
     *
     * @see Function#lastError()
     */
    public static void clearLastError()
    {
        LastError.setLastError( 0 );
    }

    /**
     * Writes a constant instruction using the most optimal byte-code form.
     *
     * @param methodVisitor
     *            the method visitor
     * @param value
     *            the value
     */
    public static void writeConstantInt( final MethodVisitor methodVisitor, final int value )
    {
        if ( value >= -1 && value <= 5 )
        {
            methodVisitor.visitInsn( ICONST_0 + value );
        }
        else if ( value >= -128 && value <= 127 )
        {
            methodVisitor.visitIntInsn( BIPUSH, value );
        }
        else if ( value >= -32768 && value <= 32767 )
        {
            methodVisitor.visitIntInsn( SIPUSH, value );
        }
        else
        {
            methodVisitor.visitLdcInsn( value );
        }
    }

    /**
     * Mangles the name until it finds a class that's not present in the given
     * class loader.
     *
     * @param name
     *            the suggested class name
     * @param loader
     *            the class loader
     * @return the actual class name
     */
    public static String getUnusedClassName( final String name,
                                             final ClassLoader loader )
    {
        if ( isClassNotPresent( loader, name ) )
        {
            return name;
        }

        long index = 0;
        while ( !isClassNotPresent( loader, name + index ) )
        {
            index++;
        }

        return name + index;
    }

    /**
     * <p>
     *  Determines the name of the library implementation type.
     * </p>
     *
     * @param type
     *            the library interface type
     * @return the implementation type name (byte-code style)
     */
    private static String getLibraryImplName( final Class< ? > type )
    {
        assert type.getAnnotation( Library.class ) != null;
        return type.getName().replace( '.', '/' ) + "_Impl_";
    }

    private static boolean isClassNotPresent( final ClassLoader loader, final String name )
    {
        // take a quick guess with a resource -- faster than loading a class
        if ( loader.getResource( name + ".class" ) != null )
        {
            return false;
        }

        // try to load it
        try
        {
            Class.forName( name.replace( '/', '.' ), false, loader );
        }
        catch ( ClassNotFoundException exception )
        {
            return true;
        }

        return false;
    }

    private static NativeStringBuffer createStringBufferImpl( final int capacity,
                                                              final FastCharset fastCharset,
                                                              final Pointer pointer )
    {
        NativeStringBuffer stringBuffer;
        switch ( fastCharset.getCodeSize() )
        {
            case 1:
                stringBuffer = new ByteNativeStringBuffer( pointer, capacity, fastCharset );
                break;

            case 2:
                stringBuffer =  new ShortNativeStringBuffer( pointer, capacity, fastCharset );
                break;

            case 4:
                stringBuffer =  new IntNativeStringBuffer( pointer, capacity, fastCharset );
                break;

            default:
                throw new AssertionError( fastCharset.getCodeSize() );
        }
        return stringBuffer;
    }

    private static void dumpByteCode( final String name, final byte[] classFile )
    {
        LOGGER.fine( "Dumping class file for " + name );

        File file = new File( DUMP_BYTE_CODE_DIR, name.replace( '.', File.separatorChar ) + ".class" );
        File parent = file.getParentFile();
        if ( !parent.mkdirs() && !parent.isDirectory() )
        {
            LOGGER.fine( "Cannot create dump directory: " + parent );
        }

        FileOutputStream output = null;
        try
        {
            output = new FileOutputStream( file );
            output.write( classFile );
        }
        catch ( IOException exception )
        {
            LOGGER.log( Level.WARNING, "Failed to dump class for " + name, exception );
        }
        finally
        {
            if ( output != null )
            {
                try
                {
                    output.close();
                }
                catch ( IOException exception )
                {
                    LOGGER.log( Level.WARNING, "Failed to dump class for " + name, exception );
                }
            }
        }
    }

}
